/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.startup;

import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.deploy.ApplicationListener;
import org.apache.tomcat.JarScanner;
import org.apache.tomcat.JarScannerCallback;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.descriptor.DigesterFactory;
import org.apache.tomcat.util.descriptor.XmlErrorHandler;
import org.apache.tomcat.util.digester.Digester;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.util.scan.Jar;
import org.apache.tomcat.util.scan.JarFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.servlet.ServletContext;
import javax.servlet.descriptor.TaglibDescriptor;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.util.*;

/**
 * Startup event listener for a <b>Context</b> that configures application
 * listeners configured in any TLD files.
 *
 * @author Craig R. McClanahan
 * @author Jean-Francois Arcand
 * @author Costin Manolache
 */
public final class TldConfig implements LifecycleListener {

	private static final String TLD_EXT = ".tld";
	private static final String WEB_INF = "/WEB-INF/";
	private static final String WEB_INF_LIB = "/WEB-INF/lib/";
	private static final org.apache.juli.logging.Log log =
			org.apache.juli.logging.LogFactory.getLog(TldConfig.class);
	/**
	 * The string resources for this package.
	 */
	private static final StringManager sm =
			StringManager.getManager(Constants.Package);
	// Names of JARs that are known not to contain any TLDs
	private static volatile Set<String> noTldJars = null;
	/**
	 * The <code>Digester</code>s available to process tld files.
	 */
	private static Digester[] tldDigesters = new Digester[4];

	static {
		// Set the default list of JARs to skip for TLDs
		StringBuilder jarList = new StringBuilder(System.getProperty(
				Constants.DEFAULT_JARS_TO_SKIP, ""));

		String tldJars = System.getProperty(Constants.TLD_JARS_TO_SKIP, "");
		if (tldJars.length() > 0) {
			if (jarList.length() > 0) {
				jarList.append(',');
			}
			jarList.append(tldJars);
		}

		if (jarList.length() > 0) {
			setNoTldJars(jarList.toString());
		}
	}

	/**
	 * The Context we are associated with.
	 */
	private Context context = null;
	/**
	 * The <code>Digester</code> we will use to process tag library
	 * descriptor files.
	 */
	private Digester tldDigester = null;


	// ----------------------------------------------------- Instance Variables
	/**
	 * Set of URIs discovered for the associated context. Used to enforce the
	 * correct processing priority. Only the TLD associated with the first
	 * instance of any URI will be processed.
	 */
	private Set<String> taglibUris = new HashSet<String>();
	private Set<String> webxmlTaglibUris = new HashSet<String>();
	private ArrayList<String> listeners = new ArrayList<String>();

	/**
	 * Create (if necessary) and return a Digester configured to process the
	 * tld.
	 */
	private static synchronized Digester createTldDigester(boolean validation,
	                                                       boolean blockExternal) {

		Digester digester;
		int cacheIndex = 0;
		if (validation) {
			cacheIndex += 1;
		}
		if (blockExternal) {
			cacheIndex += 2;
		}
		digester = tldDigesters[cacheIndex];
		if (digester == null) {
			digester = DigesterFactory.newDigester(validation,
					true, new TldRuleSet(), blockExternal);
			digester.getParser();
			tldDigesters[cacheIndex] = digester;
		}
		return digester;
	}

	/**
	 * Sets the list of JARs that are known not to contain any TLDs.
	 *
	 * @param jarNames List of comma-separated names of JAR files that are
	 *                 known not to contain any TLDs.
	 */
	public static synchronized void setNoTldJars(String jarNames) {
		if (jarNames == null) {
			noTldJars = null;
		} else {
			if (noTldJars == null) {
				noTldJars = new HashSet<String>();
			} else {
				noTldJars.clear();
			}
			StringTokenizer tokenizer = new StringTokenizer(jarNames, ",");
			while (tokenizer.hasMoreElements()) {
				String token = tokenizer.nextToken().trim();
				if (token.length() > 0) {
					noTldJars.add(token);
				}
			}
		}
	}

	// --------------------------------------------------------- Public Methods

	/**
	 * Adds a taglib URI to the list of known URIs.
	 */
	public void addTaglibUri(String uri) {
		taglibUris.add(uri);
	}

	/**
	 * Determines if the provided URI is a known taglib URI.
	 */
	public boolean isKnownTaglibUri(String uri) {
		return taglibUris.contains(uri);
	}

	/**
	 * Determines if the provided URI is a known taglib URI.
	 */
	public boolean isKnownWebxmlTaglibUri(String uri) {
		return webxmlTaglibUris.contains(uri);
	}

	/**
	 * @deprecated Unused - will be removed in 8.0.x
	 */
	@Deprecated
	public Context getContext() {
		return context;
	}

	/**
	 * @deprecated Unused - will be removed in 8.0.x
	 */
	@Deprecated
	public void setContext(Context context) {
		this.context = context;
	}

	public void addApplicationListener(String s) {
		if (log.isDebugEnabled())
			log.debug("Add tld listener " + s);
		listeners.add(s);
	}

	public String[] getTldListeners() {
		String result[] = new String[listeners.size()];
		listeners.toArray(result);
		return result;
	}

	/**
	 * Scan for and configure all tag library descriptors found in this
	 * web application.
	 * <p>
	 * This supports a Tomcat-specific extension to the TLD search
	 * order defined in the JSP spec. It allows tag libraries packaged as JAR
	 * files to be shared by web applications by simply dropping them in a
	 * location that all web applications have access to (e.g.,
	 * <CATALINA_HOME>/lib). It also supports some of the weird and
	 * wonderful arrangements present when Tomcat gets embedded.
	 * <p>
	 * The set of shared JARs to be scanned for TLDs is narrowed down by
	 * the <tt>noTldJars</tt> class variable, which contains the names of JARs
	 * that are known not to contain any TLDs.
	 */
	@SuppressWarnings("deprecation") // Context.addApplicationListener(ApplicationListener) is deprecated.
	public void execute() {
		long t1 = System.currentTimeMillis();

        /*
         * Priority order of URIs required by spec is:
         * 1. J2EE platform taglibs - Tomcat doesn't provide these
         * 2. web.xml entries
         * 3. JARS in WEB-INF/lib & TLDs under WEB-INF (equal priority)
         * 4. Additional entries from the container
         * 
         * Keep processing order in sync with o.a.j.compiler.TldLocationsCache
         */

		// Stage 2 - web.xml entries
		tldScanWebXml();

		// Stage 3a - TLDs under WEB-INF (not lib or classes)
		tldScanResourcePaths(WEB_INF);

		// Stages 3b & 4
		JarScanner jarScanner = context.getJarScanner();

		TldJarScannerCallback tldCallBack = new TldJarScannerCallback();
		jarScanner.scan(context.getServletContext(), context.getLoader().getClassLoader(),
				tldCallBack, noTldJars);
		if (tldCallBack.scanFoundNoTLDs()) {
			log.info(sm.getString("tldConfig.noTldSummary"));
		}
		// Now add all the listeners we found to the listeners for this context
		String list[] = getTldListeners();

		if (log.isDebugEnabled())
			log.debug(sm.getString("tldConfig.addListeners",
					Integer.valueOf(list.length)));

		for (int i = 0; i < list.length; i++) {
			context.addApplicationListener(
					new ApplicationListener(list[i], true));
		}

		long t2 = System.currentTimeMillis();
		if (context instanceof StandardContext) {
			((StandardContext) context).setTldScanTime(t2 - t1);
		}

	}

	/**
	 * Get the taglib entries from web.xml and add them to the map.
	 * <p>
	 * This is not kept in sync with o.a.j.compiler.TldLocationsCache as this
	 * code needs to scan the TLDs listed in web.xml whereas Jasper only needs
	 * the URI to TLD mappings.
	 */
	private void tldScanWebXml() {

		if (log.isTraceEnabled()) {
			log.trace(sm.getString("tldConfig.webxmlStart"));
		}

		Collection<TaglibDescriptor> descriptors =
				context.getJspConfigDescriptor().getTaglibs();

		for (TaglibDescriptor descriptor : descriptors) {
			String resourcePath = descriptor.getTaglibLocation();
			// Note: Whilst the Servlet 2.4 DTD implies that the location must
			// be a context-relative path starting with '/', JSP.7.3.6.1 states
			// explicitly how paths that do not start with '/' should be
			// handled.
			if (!resourcePath.startsWith("/")) {
				resourcePath = WEB_INF + resourcePath;
			}
			if (taglibUris.contains(descriptor.getTaglibURI())) {
				log.warn(sm.getString("tldConfig.webxmlSkip", resourcePath,
						descriptor.getTaglibURI()));
			} else {
				if (log.isTraceEnabled()) {
					log.trace(sm.getString("tldConfig.webxmlAdd", resourcePath,
							descriptor.getTaglibURI()));
				}
				InputStream stream = null;
				try {
					stream = context.getServletContext().getResourceAsStream(
							resourcePath);
					if (stream != null) {
						XmlErrorHandler handler = tldScanStream(stream);
						handler.logFindings(log, resourcePath);
						taglibUris.add(descriptor.getTaglibURI());
						webxmlTaglibUris.add(descriptor.getTaglibURI());
					} else {
						log.warn(sm.getString("tldConfig.webxmlFailPathDoesNotExist", resourcePath,
								descriptor.getTaglibURI()));
					}
				} catch (IOException ioe) {
					log.warn(sm.getString("tldConfig.webxmlFail", resourcePath,
							descriptor.getTaglibURI()), ioe);
				} finally {
					if (stream != null) {
						try {
							stream.close();
						} catch (Throwable t) {
							ExceptionUtils.handleThrowable(t);
						}
					}
				}
			}
		}
	}

	// -------------------------------------------------------- Private Methods

	/*
	 * Scans the web application's sub-directory identified by startPath,
	 * along with its sub-directories, for TLDs.
	 *
	 * Initially, rootPath equals /WEB-INF/. The /WEB-INF/classes and
	 * /WEB-INF/lib sub-directories are excluded from the search, as per the
	 * JSP 2.0 spec.
	 *
	 * Keep in sync with o.a.j.comiler.TldLocationsCache
	 */
	private void tldScanResourcePaths(String startPath) {

		if (log.isTraceEnabled()) {
			log.trace(sm.getString("tldConfig.webinfScan", startPath));
		}

		ServletContext ctxt = context.getServletContext();

		Set<String> dirList = ctxt.getResourcePaths(startPath);
		if (dirList != null) {
			Iterator<String> it = dirList.iterator();
			while (it.hasNext()) {
				String path = it.next();
				if (!path.endsWith(TLD_EXT)
						&& (path.startsWith(WEB_INF_LIB)
						|| path.startsWith("/WEB-INF/classes/"))) {
					continue;
				}
				if (path.endsWith(TLD_EXT)) {
					if (path.startsWith("/WEB-INF/tags/") &&
							!path.endsWith("implicit.tld")) {
						continue;
					}
					InputStream stream = ctxt.getResourceAsStream(path);
					try {
						XmlErrorHandler handler = tldScanStream(stream);
						handler.logFindings(log, path);
					} catch (IOException ioe) {
						log.warn(sm.getString("tldConfig.webinfFail", path),
								ioe);
					} finally {
						if (stream != null) {
							try {
								stream.close();
							} catch (Throwable t) {
								ExceptionUtils.handleThrowable(t);
							}
						}
					}
				} else {
					tldScanResourcePaths(path);
				}
			}
		}
	}

	/*
	 * Scans the directory identified by startPath, along with its
	 * sub-directories, for TLDs.
	 *
	 * Keep in sync with o.a.j.comiler.TldLocationsCache
	 */
	private boolean tldScanDir(File start) {
		boolean isFound = false;

		if (log.isTraceEnabled()) {
			log.trace(sm.getString("tldConfig.dirScan", start.getAbsolutePath()));
		}

		File[] fileList = start.listFiles();
		if (fileList != null) {
			for (int i = 0; i < fileList.length; i++) {
				// Scan recursively
				if (fileList[i].isDirectory()) {
					tldScanDir(fileList[i]);
				} else if (fileList[i].getAbsolutePath().endsWith(TLD_EXT)) {
					InputStream stream = null;
					isFound = true;
					try {
						stream = new FileInputStream(fileList[i]);
						XmlErrorHandler handler = tldScanStream(stream);
						handler.logFindings(log, fileList[i].getAbsolutePath());
					} catch (IOException ioe) {
						log.warn(sm.getString("tldConfig.dirFail",
								fileList[i].getAbsolutePath()),
								ioe);
					} finally {
						if (stream != null) {
							try {
								stream.close();
							} catch (Throwable t) {
								ExceptionUtils.handleThrowable(t);
							}
						}
					}
				}
			}
		}
		if (!isFound) {
			if (log.isDebugEnabled()) {
				log.debug(sm.getString("tldConfig.noTldInDir", start.getAbsolutePath()));
			}
		}
		return isFound;
	}

	/*
	 * Scans the given JarURLConnection for TLD files located in META-INF
	 * (or a sub-directory of it).
	 *
	 * @param jarConn The JarURLConnection to the JAR file to scan
	 *
	 * Keep in sync with o.a.j.comiler.TldLocationsCache
	 */
	private boolean tldScanJar(JarURLConnection jarConn) {

		Jar jar = null;
		InputStream is;
		boolean isFound = false;

		try {
			jar = JarFactory.newInstance(jarConn.getURL());

			jar.nextEntry();
			String entryName = jar.getEntryName();
			while (entryName != null) {
				if (entryName.startsWith("META-INF/") &&
						entryName.endsWith(".tld")) {
					isFound = true;
					is = null;
					try {
						is = jar.getEntryInputStream();
						XmlErrorHandler handler = tldScanStream(is);
						handler.logFindings(log, jarConn.getURL() + entryName);
					} finally {
						if (is != null) {
							try {
								is.close();
							} catch (IOException ioe) {
								// Ignore
							}
						}
					}
				}
				jar.nextEntry();
				entryName = jar.getEntryName();
			}
			if (!isFound) {
				if (log.isDebugEnabled()) {
					log.debug(sm.getString("tldConfig.noTldInJar",
							jarConn.getURL().getFile()));
				}
			}
		} catch (IOException ioe) {
			log.warn(sm.getString("tldConfig.jarFail", jarConn.getURL()), ioe);
		} finally {
			if (jar != null) {
				jar.close();
			}
		}
		return isFound;
	}

	/*
	 * Scan the TLD contents in the specified input stream, and register
	 * any application event listeners found there.  <b>NOTE</b> - This
	 * method ensure that the InputStream is correctly closed.
	 *
	 * @param resourceStream InputStream containing a tag library descriptor
	 *
	 * @throws IOException  If the file cannot be read
	 */
	private XmlErrorHandler tldScanStream(InputStream resourceStream)
			throws IOException {

		InputSource source = new InputSource(resourceStream);

		XmlErrorHandler result = new XmlErrorHandler();

		synchronized (tldDigester) {
			try {
				tldDigester.setErrorHandler(result);
				tldDigester.push(this);
				tldDigester.parse(source);
			} catch (SAXException s) {
				// Hack - makes exception handling simpler
				throw new IOException(s);
			} finally {
				tldDigester.reset();
			}
			return result;
		}
	}

	@Override
	public void lifecycleEvent(LifecycleEvent event) {
		// Identify the context we are associated with
		try {
			context = (Context) event.getLifecycle();
		} catch (ClassCastException e) {
			log.error(sm.getString("tldConfig.cce", event.getLifecycle()), e);
			return;
		}

		if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
			init();
		} else if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
			try {
				execute();
			} catch (Exception e) {
				log.error(sm.getString(
						"tldConfig.execute", context.getName()), e);
			}
		} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
			taglibUris.clear();
			webxmlTaglibUris.clear();
			listeners.clear();
		}
	}

	private void init() {
		if (tldDigester == null) {
			tldDigester = createTldDigester(context.getTldValidation(),
					context.getXmlBlockExternal());
		}
	}

	private class TldJarScannerCallback implements JarScannerCallback {
		boolean tldFound = true;

		@Override
		public void scan(JarURLConnection urlConn) throws IOException {
			tldFound = tldScanJar(urlConn);
		}

		@Override
		public void scan(File file) {
			File metaInf = new File(file, "META-INF");
			if (metaInf.isDirectory()) {
				tldFound = tldScanDir(metaInf);
			}
		}

		private boolean scanFoundNoTLDs() {
			return !tldFound;
		}
	}

}
